﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UTSDAL.LINQSQL.Model;
using UTSDAL.LINQSQL.Views;
using UTSDAL.SQL;
using System.Data.SqlClient;
using System.Data;
using UTSDAL.LINQSQL.Exceptions;
using log4net;
using UTSDAL.LINQSQL.Utils;

namespace UTSDAL.LINQSQL.DAO
{
    public class TimetableEventDAO:GenericDAO<TimetableEvent,Guid>
    {

        private static readonly ILog log = LogManager.GetLogger(typeof(TimetableEventDAO));

        private TimetableEventTypeDAO timetableEventTypeDAO = new TimetableEventTypeDAO();

        private ModuleDAO moduleDAO = new ModuleDAO();

        public override TimetableEvent  findById(Guid id)
        {
            var timetableEventVar = from e in base.Context.TimetableEvents
                                    where e.TimetableEventId.Equals(id)
                                    select e;

            return timetableEventVar.SingleOrDefault<TimetableEvent>();
        }

        private ConnectionManager connectionManager = ConnectionManager.getInstance();

        public override void insert(UTSDAL.LINQSQL.Model.TimetableEvent instance)
        {
            this.query(instance, SqlOperationType.INSERT);
        }

        public override void save(UTSDAL.LINQSQL.Model.TimetableEvent instance)
        {
            this.query(instance, SqlOperationType.UPDATE);
        }

        public override void remove(TimetableEvent instance)
        {
            this.query(instance, SqlOperationType.DELETE);
        }

        public void query(UTSDAL.LINQSQL.Model.TimetableEvent instance, SqlOperationType sqlOperationType)
        {
            //  base.insert(instance);

            // Implementing normal sql insert
            StringBuilder query = new StringBuilder();
            List<SqlParameter> parameters = new List<SqlParameter>();
            SqlParameter TimetableEventId = new SqlParameter("@TimetableEventId", SqlDbType.UniqueIdentifier);
            
            if (sqlOperationType.Equals(SqlOperationType.INSERT))
            {
                query.Append("INSERT INTO TIMETABLEEVENT VALUES (");
                query.Append("@TimetableEventId,@CohortId,@SemesterId,@ModuleId,@RoomId,@ModuleEventId,@StartTime,@EndTime,@CourseDayId,@TimetableEventTypeId");
                query.Append(")");
                
                TimetableEventId.Value = Guid.NewGuid();                
            }
            else if(sqlOperationType.Equals(SqlOperationType.UPDATE))
            {
                query.Append("UPDATE TIMETABLEEVENT");
                query.Append(" SET TimetableEventId = @TimetableEventId");
                query.Append(" ,CohortId = @CohortId");
                query.Append(" ,SemesterId = @SemesterId");
                query.Append(" ,ModuleId = @ModuleId");
                query.Append(" ,RoomId = @RoomId");
                query.Append(" ,ModuleEventId = @ModuleEventId");
                query.Append(" ,StartTime = @StartTime");
                query.Append(" ,EndTime = @EndTime");
                query.Append(" ,CourseDayId = @CourseDayId");
                query.Append(" ,TimetableEventTypeId = @TimetableEventTypeId");
                query.Append(" WHERE TimetableEventId = @TimetableEventId");

                TimetableEventId.Value = instance.TimetableEventId;

            }
            else if (sqlOperationType.Equals(SqlOperationType.DELETE))
            {
                query.Append("DELETE FROM TIMETABLEEVENT");
                query.Append(" WHERE TimetableEventId = @TimetableEventId");

                TimetableEventId.Value = instance.TimetableEventId;
            }



            parameters.Add(TimetableEventId);



            SqlParameter CohortId = new SqlParameter("@CohortId", SqlDbType.UniqueIdentifier);
            CohortId.Value = instance.CohortId;
            parameters.Add(CohortId);

            SqlParameter SemesterId = new SqlParameter("@SemesterId", SqlDbType.UniqueIdentifier);
            SemesterId.Value = instance.SemesterId;
            parameters.Add(SemesterId);

            SqlParameter ModuleId = new SqlParameter("@ModuleId", SqlDbType.UniqueIdentifier);
            ModuleId.Value = instance.ModuleId;
            parameters.Add(ModuleId);

            SqlParameter RoomId = new SqlParameter("@RoomId", SqlDbType.UniqueIdentifier);
            RoomId.Value = instance.RoomId;
            parameters.Add(RoomId);

            SqlParameter ModuleEventId = new SqlParameter("@ModuleEventId", SqlDbType.UniqueIdentifier);
            ModuleEventId.Value = instance.ModuleEventId;
            parameters.Add(ModuleEventId);

            SqlParameter StartTime = new SqlParameter("@StartTime", SqlDbType.Time);
            StartTime.Value = instance.StartTime;
            parameters.Add(StartTime);

            SqlParameter EndTime = new SqlParameter("@EndTime", SqlDbType.Time);
            EndTime.Value = instance.EndTime;
            parameters.Add(EndTime);

            SqlParameter CourseDayId = new SqlParameter("@CourseDayId", SqlDbType.UniqueIdentifier);
            CourseDayId.Value = instance.CourseDayId;
            parameters.Add(CourseDayId);

            SqlParameter TimetableEventTypeId = new SqlParameter("@TimetableEventTypeId", SqlDbType.UniqueIdentifier);
            TimetableEventTypeId.Value = instance.TimetableEventTypeId;
            parameters.Add(TimetableEventTypeId);

            connectionManager.update(query.ToString(), parameters);

        }

        public List<TimetableEvent> findTimetableEventsByModuleId(Guid moduleId)
        {
              return findTimetableEventsForModule(moduleDAO.findById(moduleId));
        }


        public List<TimetableEvent> getTimetableEventsForSemester(Semester semester)
        {
            var eventsForSemester = from e in base.Context.TimetableEvents
                                    where e.Semester.Equals(semester)
                                    select e;

            return eventsForSemester.ToList<TimetableEvent>();
        }

        public Boolean doesTimetableEventsContainModuleEventForSemester(ModuleEvent moduleEvent, Semester semester)
        {

            Boolean contains = false;

            foreach (TimetableEvent timetablEvent in getTimetableEventsForSemester(semester))
            {
                if(timetablEvent.ModuleEvent.Equals(moduleEvent))
                {
                    contains = true;
                    break;
                }
            }

            return contains;
        }

        /// <summary>
        /// checks whether a module event is found for a given list of timetable events
        /// </summary>
        /// <param name="moduleEvent"></param>
        /// <param name="events"></param>
        /// <returns></returns>
        public Boolean doesTimetableEventsContainModuleEvent(ModuleEvent moduleEvent, List<TimetableEvent> events)
        {

            Boolean contains = false;

            foreach (TimetableEvent timetablEvent in events)
            {
                if (timetablEvent.ModuleEvent.Equals(moduleEvent))
                {
                    contains = true;
                    break;
                }
            }

            return contains;
        }




        public List<TimetableEvent> findTimetableEventsForModule(Module module)
        {
            var eventsVar = from e in base.Context.TimetableEvents
                            where e.Module.Equals(module)
                            select e;

            List<TimetableEvent> events = eventsVar.ToList<TimetableEvent>();

            if (events == null || events.Count < module.ModuleEvents.Count)
            {
                if (events == null)
                {
                    events = new List<TimetableEvent>();
                }

                foreach(ModuleEvent moduleEvent in module.ModuleEvents)
                {
                    if(!doesTimetableEventsContainModuleEvent(moduleEvent,events))
                    {
                        TimetableEvent timetableEvent = new TimetableEvent();
                        timetableEvent.Module = module;
                        timetableEvent.ModuleEvent = moduleEvent;
                        events.Add(timetableEvent);
                    }
                }
            }

            return events;
        }

        public List<TimetableEvent> getAllReservationEvents(Semester semester)
        {
            var reservationEventsVar = from reservation in base.Context.TimetableEvents
                                       where reservation.TimetableEventType.Equals(timetableEventTypeDAO.getReservationEventType())
                                             && reservation.SemesterId.Equals(semester.SemesterId)
                                       select reservation;

            return reservationEventsVar.ToList<TimetableEvent>();
        }

        // The Key value in this dictionary will consist of appending semesterId;cohortId and moduleId
        private Dictionary<String,List<TimetableEvent>> reservationMap = new Dictionary<String,List<TimetableEvent>>();
        
        public List<ReservationView> getAllReservationViews(Semester semester)
        {
            List<TimetableEvent> events = getAllReservationEvents(semester);

            List<ReservationView> views = new List<ReservationView>();

            foreach(TimetableEvent timetableEvent in events)
            {
                String timetableEventKey = timetableEvent.SemesterId + ";" + timetableEvent.CohortId + ";" + timetableEvent.ModuleId;

                if (reservationMap.ContainsKey(timetableEventKey))
                {
                    reservationMap[timetableEventKey].Add(timetableEvent);
                }
                else
                {
                    List<TimetableEvent> tempEvents = new List<TimetableEvent>();
                    tempEvents.Add(timetableEvent);
                    reservationMap.Add(timetableEventKey, tempEvents);
                }
            }

            foreach (KeyValuePair<String, List<TimetableEvent>> mapValue in reservationMap)
            {

                ReservationView view = new ReservationView();

                foreach (TimetableEvent e in mapValue.Value)
                {
                    view.Cohort = e.Cohort;
                    view.Module = e.Module;
                    view.CourseDay = e.CourseDay;
                    view.Semester = e.Semester;

                    if (view.ReservedEvent == null)
                    {
                        view.ReservedEvent = new List<TimetableEvent>();
                    }

                    view.ReservedEvent.Add(e);
                }

                views.Add(view);
            }

            return views;
        }

        /// <summary>
        /// checks whether room is allocated during between start and end time of the event
        /// to any other module on that particular day
        /// </summary>
        /// <param name="timetableEvent"></param>
        /// <returns></returns>
        public Boolean isTimetableEventExistingForAnyModule(TimetableEvent timetableEvent)
        {

            var timetableEventExistingVar = from e in base.Context.TimetableEvents
                                      where e.Semester.Equals(timetableEvent.Semester)
                                      && e.CourseDay.Equals(timetableEvent.CourseDay)
                                      && e.EndTime >= timetableEvent.StartTime
                                      && e.Room.Equals(timetableEvent.Room)
                                      select e;

            List<TimetableEvent> existingTimetableEvents = (List<TimetableEvent>)timetableEventExistingVar.ToList<TimetableEvent>();

            return existingTimetableEvents.Count > 0;
        }



        public Boolean isCohortBusy(TimetableEvent timetableEvent)
        {

            var cohortEvents = from e in base.Context.TimetableEvents
                               where e.CourseDay.Equals(timetableEvent.CourseDay)
                               && timetableEvent.StartTime >= e.StartTime
                               && timetableEvent.StartTime < e.EndTime
                               && timetableEvent.Semester.Equals(e.Semester)
                               && timetableEvent.Cohort.Equals(e.Cohort)
                               select e;


            return (cohortEvents.ToList<TimetableEvent>().Count > 0 );

        }

        /// <summary>
        /// checks whether a reservation has not already been made during the given timeslot on the chosen room
        /// </summary>
        /// <param name="timetableEvent"></param>
        /// <returns></returns>

        public Boolean isReservationValid(TimetableEvent timetableEvent)
        {

            var reservationValidVar = from e in base.Context.TimetableEvents
                                      where e.CourseDay.Equals(timetableEvent.CourseDay)
                                      && e.Semester.Equals(timetableEvent.Semester)
                                      && timetableEvent.StartTime>=e.StartTime
                                      && timetableEvent.StartTime>=e.EndTime
                                      select e;

            List<TimetableEvent> existingTimetableEvents = (List<TimetableEvent>)reservationValidVar.ToList<TimetableEvent>();
                        
            return existingTimetableEvents.Count > 0;
        }

        

        /// <summary>
        /// retrieves a list of unallocated module events for a cohort in a given semester
        /// </summary>
        /// <param name="semester"></param>
        /// <param name="cohort"></param>
        /// <returns></returns>
        public List<ModuleEvent> getUnallocatedModuleEventsBySemesterAndCohort(Semester semester, Cohort cohort)
        {

            List<ModuleEvent> unallocatedModuleEvents = null;

            var semesterModulesVar = from cohortModule in cohort.CohortModules
                                     where cohortModule.Semester.Equals(semester)
                                     select cohortModule;

            
            foreach(CohortModule cohortModule in semesterModulesVar.ToList<CohortModule>())
            {
                foreach (ModuleEvent moduleEvent in cohortModule.Module.ModuleEvents)
                {
                    if (!doesTimetableEventsContainModuleEventForSemester(moduleEvent,semester))
                    {
                        if (unallocatedModuleEvents == null)
                        {
                            unallocatedModuleEvents = new List<ModuleEvent>();
                        }
                        else
                        {
                            unallocatedModuleEvents.Add(moduleEvent);
                        }
                    }
                }
            }

            return unallocatedModuleEvents;
        }


        private RoomDAO roomDAO = new RoomDAO();

        /// <summary>
        /// looks for a timetable event that fits the module event by resolving
        /// all constraints and facilities 
        /// </summary>
        /// <param name="moduleEvent"></param>
        /// <returns></returns>
        public TimetableEvent getBestFitTimetableEvent(ModuleEvent moduleEvent)
        {
            return null;
        }

        public List<TimetableEvent> getUsedTimetableEvents(ModuleEvent moduleEvent)
        {
            List<TimetableEvent> usedTimetableEvents = new List<TimetableEvent>();



            foreach (CourseDay day in base.Context.CourseDays)
            {
                var usedTimetableEventQuery = from e in base.Context.TimetableEvents
                                              where
                                              (!e.CourseDay.Equals(day))
                                              &&                                              
                                              (!e.Room.Equals(roomDAO.getRoomForModuleEvent(moduleEvent)))
                                              select e;

                usedTimetableEvents = usedTimetableEvents.ToList<TimetableEvent>();
            }

            return usedTimetableEvents;
        }

        

        public TimetableEvent generate(Semester semester,Cohort cohort,ModuleEvent moduleEvent)
        {

            log.Debug("DAO Generating Timetable");

            TimetableEvent timetableEvent = new TimetableEvent();
            timetableEvent.ModuleEvent = moduleEvent;
            timetableEvent.Cohort = cohort;
            timetableEvent.Semester = semester;
            
            //TODO: Set Cohort of timetableEvent and then look for lecture and cohort availability


            List<CourseDay> enabledCourseDays = courseDayDAO.getEnabledCourseDays();
            enabledCourseDays = ObjectUtils.Randomize<CourseDay>(enabledCourseDays);

            foreach (CourseDay courseDay in enabledCourseDays)
            {
                timetableEvent.CourseDay = courseDay;
                Exception generationException = null;

                // If All generation attempts failed for that particular day, then we try to generate for another day
                while ((!(generationException is ExceedingMatchingTimeException)))
                {
                    timetableEvent.CourseDay = courseDay;

                    try
                    {
                        /* We dont re generate a time for that event if previous generation failed due to:
                         *  - room not being available                         
                         * 
                         * OR
                         * 
                         * first time that we try to generate a timetable event (generationException is NULL)
                         * 
                         */ 
                        if ((generationException == null) || (generationException is RoomNotAvailableException) || (generationException is CohortNotAvailableException))
                        {
                            timetableEvent = getNextMatchingTime(timetableEvent);
                        }

                        // If a start time and end time has been correctly allocated to the event
                        if (timetableEvent != null)
                        {
                            // Looking for a FREE room in that time slot for that event
                            timetableEvent = roomDAO.allocateRoomToTimetableEvent(timetableEvent);
                        }


                        // If a room has been allocated to that event, then we proceed
                        if (timetableEvent != null)
                        {
                            //checking cohort Availability
                            if (isCohortBusy(timetableEvent))
                            {
                                log.Debug("Cohort Busy at " + timetableEvent.CourseDay.CourseDayName + "(" + timetableEvent.StartTime + "-" + timetableEvent.EndTime + ")");
                                generationException = new CohortNotAvailableException("Cohort is Busy at that time");
                            }
                            else
                            {
                                log.Debug("Cohort NOT Busy at " + timetableEvent.CourseDay.CourseDayName + "(" + timetableEvent.StartTime + "-" + timetableEvent.EndTime + ")");
                                generationException = null;
                            }
                            
                        }

                        //if cohort is not busy && timetable has been succesfully generated, then we look at the constraints
                        if (timetableEvent != null)
                        {
                            //ConstraintValidators.validate(timetableEvent);
                        }
                    }
                    catch (Exception e)
                    {
                        generationException = e;
                    }

                    // we break if no exception is generated, meaning timetable generation for this event has been successfull
                    if (generationException == null)
                    {
                        break;
                    }
                }


                // we break if no exception is generated again, meaning timetable generation for this event has been successfull
                if (generationException == null)
                {
                    timetableEvent.Module = moduleEvent.Module;
                    timetableEvent.Cohort = moduleEvent.Cohort;
                    timetableEvent.Semester = semester;

                    timetableEvent.TimetableEventType = timetableEventTypeDAO.getGenerationEyentType();
                    insert(timetableEvent);
                }

            }

            return timetableEvent;
        }

        /*
        public TimetableEvent allocateTimeToTimetableEvent(TimetableEvent timetableEvent)
        {
            GenerationResult generationResult = new GenerationResult();
            generationResult.TimetableEvent = timetableEvent;

            // Attempting to find a new Matching time
            try
            {
                generationResult.TimetableEvent = getNextMatchingTime(timetableEvent);
            }
            catch (Exception e)
            {
                // If the Matching returns an error
                generationResult.GenerationException = e;
            }


            // If the Error is because of exceeding Time (Ending Time)
            // Then generate by looking for next matching time
            if ((generationResult.GenerationException is ExceedingMatchingTimeException))
            {
                generationResult.TimetableEvent = (generationResult.TimetableEvent);
            }


            // If no matching time could be found for the whole week, then we cannot proceed 
            // Thus we set the timetable event to null so that the calling method 
            // recognises that it cannot proceed either
            if (generationResult.GenerationException is NonMatchingTimeException)
            {
                generationResult.TimetableEvent = null;
            }

            return generationResult.TimetableEvent;

        }
        */


        public TimetableEvent allocateTimeToTimetableEvent(TimetableEvent timetableEvent)
        {
            GenerationResult generationResult = new GenerationResult();
            generationResult.TimetableEvent = timetableEvent;

            // Attempting to find a new Matching time
            try
            {
                generationResult.TimetableEvent = getNextMatchingTime(timetableEvent);
            }
            catch (Exception e)
            {
                // If the Matching returns an error
                generationResult.GenerationException = e;
            }


            // If the Error is because of exceeding Time (Ending Time)
            // Then generate by looking for next matching time
            if ((generationResult.GenerationException is ExceedingMatchingTimeException))
            {
                generationResult.TimetableEvent = (generationResult.TimetableEvent);
            }


            // If no matching time could be found for the whole week, then we cannot proceed 
            // Thus we set the timetable event to null so that the calling method 
            // recognises that it cannot proceed either
            if (generationResult.GenerationException is NonMatchingTimeException)
            {
                generationResult.TimetableEvent = null;
            }

            return generationResult.TimetableEvent;

        }



        private SettingsDAO settingsDAO = new SettingsDAO();

        private CourseDayDAO courseDayDAO = new CourseDayDAO();

        /// <summary>
        /// sets the start time of the timetable event to its end time 
        /// and adds the module event duration (in hours) to this new 
        /// start time so that the timetable event can be allocated a slot if any
        /// </summary>
        /// <param name="timetableEvent"></param>
        /// <returns></returns>
        public TimetableEvent getNextMatchingTime(TimetableEvent timetableEvent)
        {
            // If a matching time is attempted for the first time
            if (timetableEvent.StartTime.TotalHours == 0)
            {
                //timetableEvent.StartTime = settingsDAO.getStartTime();
                timetableEvent.StartTime = timetableEvent.CourseDay.StartTime;

                if (timetableEvent.Cohort.CohortType.StartTime > timetableEvent.StartTime)
                {
                    timetableEvent.StartTime = timetableEvent.Cohort.CohortType.StartTime;
                }

            }
            else // if a matching time is attempted after one has not been found
            {
                timetableEvent.StartTime = timetableEvent.EndTime;
            }
            
            TimeSpan duration = new TimeSpan(Convert.ToInt32(timetableEvent.ModuleEvent.TimeRequired),0,0);
            timetableEvent.EndTime = timetableEvent.StartTime.Add(duration);


            // If Timetable Event Matching Time exceeds closing time then invalidate the timetable even
            //if (timetableEvent.EndTime > settingsDAO.getEndTime())
            if (timetableEvent.EndTime > timetableEvent.CourseDay.EndTime)
            {
                // If a time match has not been found for the whole week then
                // we cannot allocate this timetable event
                if (courseDayDAO.isLastDayOfWeekForCourse(timetableEvent.CourseDay))
                {
                    // we reset the time
                    timetableEvent.StartTime = new TimeSpan();
                    timetableEvent.EndTime = new TimeSpan();
                    throw new NonMatchingTimeException("Could not find a Matching Time throughout the Whole Week ");
                }
                else
                {
                    timetableEvent.StartTime = new TimeSpan();
                    timetableEvent.EndTime = new TimeSpan();
                    throw new ExceedingMatchingTimeException("Ending Time is After closure ");
                }
            }

            return timetableEvent;
        }


        public TimetableEventView getTimetableEventView(TimetableEvent timetableEvent)
        {
            TimetableEventView view = new TimetableEventView(timetableEvent);
            return view;
        }

        public List<TimetableEventView> getTimetableEventViews(List<TimetableEvent> events)
        {

            List<TimetableEventView> views = new List<TimetableEventView>();

            foreach (TimetableEvent e in events)
            {
                views.Add(getTimetableEventView(e));
            }

            return views;
        }


        public List<TimetableEventView> getTimetableEventsViewForSemester(Semester semester)
        {

            return getTimetableEventViews(getTimetableEventsForSemester(semester));
        }

        public List<TimetableEventView> getTimetableEventsViewBySemesterAndCohort(Semester semester, Cohort cohort)
        {
            List<TimetableEventView> views = getTimetableEventsViewForSemester(semester);

            List<TimetableEventView> cohortViews = new List<TimetableEventView>();

            if (cohort == null)
            {
                cohortViews = views;
            }
            else
            {
                var cohortViewVar = from view in views
                                    where view.TimetableEvent.Cohort.Equals(cohort)
                                    select view;

                cohortViews = cohortViewVar.ToList<TimetableEventView>();
            }

            return cohortViews;
        }


        public List<TimetableEventView> getTimetableEventsViewByCriteria(Semester semester, Cohort cohort, Module module)
        {

            List<TimetableEventView> views = getTimetableEventsViewBySemesterAndCohort(semester, cohort);

            List<TimetableEventView> moduleViews = new List<TimetableEventView>();
            if (module == null)
            {
                moduleViews = views;
            }
            else
            {
                var moduleViewVar = from view in views
                                    where view.TimetableEvent.Module.Equals(module)
                                    select view;

                moduleViews = moduleViewVar.ToList<TimetableEventView>();
            }

            return moduleViews;
            
                          
        }

        /// <summary>
        /// checks whether a timetable event can be added with regards to cohort event considerations
        /// and room occupancy
        /// </summary>
        /// <param name="timetableEvent"></param>
        /// <returns></returns>
        public Boolean isTimetableEventValid(TimetableEvent timetableEvent)
        {
            
            return (!isCohortBusy(timetableEvent) && !roomDAO.isRoomBusy(timetableEvent));
        }


        /// <summary>
        /// returns all timetable events of a cohort for a specific day
        /// </summary>
        /// <param name="cohort"></param>
        /// <param name="courseDay"></param>
        /// <returns></returns>
        public List<TimetableEvent> getTimetableEventsForCohortForCourseDay(Cohort cohort, CourseDay courseDay)
        {

            var events = from e in base.Context.TimetableEvents
                         where e.CourseDay.CourseDayId.Equals(courseDay.CourseDayId)
                         && e.CohortId.Equals(cohort.CohortId)
                         select e;

            return events.ToList<TimetableEvent>();

        }

        public int getTimetableEventsDurationForCohortForCourseDay(Cohort cohort, CourseDay courseDay)
        {

            List<TimetableEvent> events = getTimetableEventsForCohortForCourseDay(cohort, courseDay);

            int duration = 0;
            
            foreach(TimetableEvent timetableEvent in events)
            {
                TimeSpan eventDuration = timetableEvent.EndTime.Subtract(timetableEvent.StartTime);
                duration += eventDuration.Hours;
            }

            return duration;
        }
    }

}
